// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.gctoolkit.parser.jvm;

import com.microsoft.gctoolkit.parser.unified.UnifiedLoggingLevel;
import com.microsoft.gctoolkit.parser.unified.UnifiedLoggingTokens;
import com.microsoft.gctoolkit.time.DateTimeStamp;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;

public class Decorators {

    /**
     * This class represents the decorators found in the log line as defined by JEP 158 (https://openjdk.java.net/jeps/158).
     * The list, taken from that document is as:
     * time -- Current time and date in ISO-8601 format
     * uptime -- Time since the start of the JVM in seconds and milliseconds (e.g., 6.567s)
     * timemillis -- The same value as generated by System.currentTimeMillis()
     * uptimemillis -- Milliseconds since the JVM started
     * timenanos -- The same value as generated by System.nanoTime()
     * uptimenanos -- Nanoseconds since the JVM started
     * pid -- The process identifier
     * tid -- The thread identifier
     * level -- The level associated with the log message
     * tags -- The tag-set associated with the log message
     *
     * This implementation takes advantage of the property that the ordering of tags is stable. For example,
     * -Xlog:*::pid,time produces the identical ordering as -Xlog:*::time,pid. Moreover, the list of decorators
     * is dedupped implying that the second decorator in -Xlog:*::time,time is ignored.
     *
     * The default decorators (as of the time of authoring JDK 9-21) include [uptime][level][tags].
     * For example, [1.361s][info][gc,heap]. This reads uptime of 1.361 seconds. The tag for the log record
     * is gc.heap at the info level.
     *
     * At issue is that uptime and timemillis are, on the surface, indistinguishable. The same is true with
     * timenanos and uptimenanos as well as with pid and tid. The following logic can be used to help differentiate
     * indistinguishable decorators.
     *
     * 1) If both decorators are present then order can be used to differentiate
     * 2) If ms time value - 20 years > 0, then timemillis can be assumed. Otherwise uptime is assumed. The reasoning
     *    is, unified logging didn't exist that long ago. The value of 20 has been arbitrarily chosen.
     * 3) There is no reliable way to differentiate the nanosecond timings if only 1 is present. However this may not
     *    matter as GCToolKit would only use these values to create a baseline measure to align date/time with uptime.
     *    This technique is designed to work-around the cases where logs do not contain the uptime decorator.
     * 4) There is no known way to reliably differentiate between PID and TID. At this time, GCToolKit ignores these
     *    decorators.
     *
     * Todo: GCToolkit captures time in the DateTimeStamp class. That class will capture uptime or time or both. If both
     * are missing, GCToolkit JVMEvents will have no sense of time. It is possible that the other timing fields could fill
     * in cases where both the time and uptime decorators were missing.
     * @param line
     */

    private static final Logger LOGGER = Logger.getLogger(Decorators.class.getName());

    // This is to help differentiate between JVM running time and wall clock time.
    private static final long TWENTY_YEARS_IN_MILLIS = 731L * 24L * 60L * 60L * 1000L;
    private static final long TWENTY_YEARS_IN_NANO = 731L * 24L * 60L * 60L * 1000L;

    private static final int DATE_STAMP_GROUP = 0;
    private static final int UPTIME_GROUP = 1;
    private static final int TIME_MILLIS_OR_MAYBE_UPTIME_MILLIS_GROUP = 2;
    private static final int UPTIME_MILLIS_GROUP = 3;
    private static final int TIME_NANOS_OR_MAYBE_UPTIME_NANOS_GROUP = 4;
    private static final int UPTIME_NANOS_GROUP = 5;
    private static final int PID_GROUP = 6;
    private static final int TID_GROUP = 7;
    private static final int LOG_LEVEL_GROUP = 8;
    private static final int TAGS_GROUP = 9;

    int numberOfDecorators;

    private String[] decoratorGroups;
    private String tags;

    public Decorators(String line) {
        extractValues(line);
    }

    private void extractValues(String line) {
        if (!line.startsWith("["))
            return;

        Matcher decoratorMatcher = UnifiedLoggingTokens.DECORATORS.matcher(line);
        if (!decoratorMatcher.find()) {
            return;
        }

        // Retrieving a group from a matcher calls substring each time
        // Store all the groups in an array ahead of time to avoid paying this cost unnecessarily
        decoratorGroups = new String[decoratorMatcher.groupCount()];
        for (int i = 1; i <= decoratorMatcher.groupCount(); i++) {
            String group = decoratorMatcher.group(i);
            decoratorGroups[i-1] = group;
            if (group != null)
                numberOfDecorators++;
        }
    }

    // For some reason, ISO_DATE_TIME doesn't like that time-zone is -0100. It wants -01:00.
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    public ZonedDateTime getDateStamp() {
        try {
            String value = decoratorGroups[DATE_STAMP_GROUP];
            if (value != null) {
                TemporalAccessor temporalAccessor = formatter.parse(value.substring(1, value.length()-1));
                return ZonedDateTime.from(temporalAccessor);
            }
        } catch (NullPointerException npe) {
            LOGGER.log(Level.SEVERE, npe.getMessage(), npe);
        }
        return null;
    }

    public double getUpTime() {
        String value = decoratorGroups[UPTIME_GROUP];
        if (value != null) {
            value = value.replace(",", ".");
            return Double.parseDouble(unboxValue(value, 1));
        }
        return -1.0d;
    }

    private long extractClock(int groupIndex, long threshold) {
        long clockReading = -1L;
        String stringValue = decoratorGroups[groupIndex];
        if (stringValue != null) {
            clockReading = Long.parseLong(unboxValue(stringValue, 2));
            if (decoratorGroups[groupIndex + 1] == null)
                if (clockReading < threshold)
                    clockReading = -1L;
        }
        return clockReading;
    }

    public long getTimeMillis() {
        return extractClock(TIME_MILLIS_OR_MAYBE_UPTIME_MILLIS_GROUP, TWENTY_YEARS_IN_MILLIS);
    }

    public long getUptimeMillis() {
        String value = decoratorGroups[UPTIME_MILLIS_GROUP];
        if (value == null) {
            value = decoratorGroups[TIME_MILLIS_OR_MAYBE_UPTIME_MILLIS_GROUP];
        }
        if (value != null) {
            long longValue = Long.parseLong(unboxValue(value, 2));
            if (longValue < TWENTY_YEARS_IN_MILLIS)
                return longValue;
        }
        return -1L;
    }

    public long getTimeNano() {
        return extractClock(TIME_NANOS_OR_MAYBE_UPTIME_NANOS_GROUP, TWENTY_YEARS_IN_NANO);
    }

    public long getUptimeNano() {
        String value = decoratorGroups[UPTIME_NANOS_GROUP];
        if (value == null) {
            value = decoratorGroups[TIME_NANOS_OR_MAYBE_UPTIME_NANOS_GROUP];
        }
        if (value != null) {
            long longValue = Long.parseLong(unboxValue(value, 2));
            if (longValue < TWENTY_YEARS_IN_NANO)
                return longValue;
        }
        return -1L;
    }

    public int getPid() {
        String value = decoratorGroups[PID_GROUP];
        if (value != null) {
            return Integer.parseInt(unboxValue(value));
        }
        return -1;
    }

    public int getTid() {
        String value = decoratorGroups[TID_GROUP];
        if (value != null) {
            return Integer.parseInt(unboxValue(value));
        }
        return -1;
    }

    public Optional<UnifiedLoggingLevel> getLogLevel() {
        String level = decoratorGroups[LOG_LEVEL_GROUP];
        if (level != null)
            try {
                return Optional.of(UnifiedLoggingLevel.valueOf(unboxValue(level)));
            } catch (IllegalArgumentException e) {
                LOGGER.fine("No such debug level: " + level);
                LOGGER.fine(e.getMessage());
                return Optional.empty();
            }
        return Optional.empty();
    }

    public DateTimeStamp getDateTimeStamp() {
        return new DateTimeStamp(getDateStamp(), getUpTime());
    }

    public int getNumberOfDecorators() {
        return numberOfDecorators;
    }

    private String unboxValue(String boxedValue, int postFix) {
        return boxedValue.substring(1, boxedValue.length() - (1 + postFix));
    }

    private String unboxValue(String boxedValue) {
        return unboxValue(boxedValue, 0).trim();
    }

    public boolean tagsContain(String tagList) {
        return getTags().contains(tagList);
    }

    public String getTags() {
        if (tags == null && decoratorGroups[TAGS_GROUP] != null) {
            tags = String.join(",", Arrays.asList(
                    unboxValue(decoratorGroups[TAGS_GROUP]).trim().split(",")
            ));
        }
        return tags;
    }
}